﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using NLog;

namespace TfsGiggle.Core
{
    /// <summary>
    /// Compares two Lucene docments using the doc property
    /// </summary>
    internal class ScoreDocComparer : IEqualityComparer<ScoreDoc>
    {
        public bool Equals(ScoreDoc x, ScoreDoc y)
        {
            return x.doc == y.doc;
        }

        public int GetHashCode(ScoreDoc obj)
        {
            return obj.GetHashCode();
        }
    }

    /// <summary>
    /// Model handling Lucene and IndexWriter/Reader
    /// </summary>
    public sealed class IndexModel : IDisposable
    {
        private static Logger _logger = LogManager.GetCurrentClassLogger();
        private Analyzer _analyzer;
        private static IndexReader _reader;
        private static IndexSearcher _searcher;

        private string _luceneDirectory;
        private string[] _knownFileExtensions = new string[] { ".cs", ".js", ".h", ".cpp" };
        private CancellationToken _cancelToken;
        private ScoreDocComparer _scoreDocComparer = new ScoreDocComparer();

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="luceneDirectory"></param>
        /// <param name="analyzer"></param>
        /// <param name="tfsUri"></param>
        /// <param name="cancelToken"></param>
        public IndexModel(string luceneDirectory, Analyzer analyzer, Uri tfsUri, CancellationToken cancelToken)
        {
            _logger.Trace("luceneDirectory:{0}, tfsUri:{1}", luceneDirectory, tfsUri);
            _luceneDirectory = luceneDirectory;
            _analyzer = analyzer;
            _cancelToken = cancelToken;

            try
            {
                var dir = new DirectoryInfo(luceneDirectory);
                var luceneDir = FSDirectory.Open(dir);
                if (!dir.Exists)
                {
                    _logger.Info("Creating new LuceneDirectory at {0}", luceneDirectory);
                    using (var writer = new IndexWriter(luceneDir, _analyzer, true, IndexWriter.MaxFieldLength.LIMITED))
                    {
                        ;
                    }

                }
                if (IndexWriter.IsLocked(luceneDir))
                {
                    _logger.Warn("LuceneDirectory {0} is locked, unlocking", luceneDirectory);
                    IndexWriter.Unlock(luceneDir);
                    _logger.Warn("LuceneDirectory {0} unlocked", luceneDirectory);
                }
                _reader = IndexReader.Open(luceneDir, true);
                _searcher = new IndexSearcher(_reader);
                TfsSourceControl.Instance.Open(tfsUri);
            }
            catch (Exception ex)
            {
                _logger.ErrorException("ctor failed", ex);
            }
        }

        /// <summary>
        /// Create a new IndexWriter
        /// </summary>
        public IndexWriter IndexWriter
        {
            get
            {
                var dir = FSDirectory.Open(new DirectoryInfo(_luceneDirectory));
                if (!IndexWriter.IsLocked(dir))
                {
                    _logger.Info("New IndexWriter created");
                    return new IndexWriter(dir, _analyzer, false, IndexWriter.MaxFieldLength.LIMITED);
                }
                _logger.Error("LuceneDirectory is currently locked");
                return null;
            }
        }

        /// <summary>
        /// Delete this document
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="sourceControlPath"></param>
        public void Delete(IndexWriter writer, string teamProjectCollectionId, string sourceControlPath)
        {
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId)) throw new ArgumentNullException("teamProjectCollectionId");
            if (string.IsNullOrWhiteSpace(sourceControlPath)) throw new ArgumentNullException("sourceControlPath");
            _logger.Trace("teamProjectCollectionId:{0} sourceControlPath:{1}", teamProjectCollectionId, sourceControlPath);

            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
            }

            try
            {
                if (Exists(teamProjectCollectionId, sourceControlPath))
                {
                    var term = new Term(SourceControlDocument.FieldNames.DocumentKey, GetDocumentKey(teamProjectCollectionId, sourceControlPath));
                    writer.DeleteDocuments(term);
                    _logger.Info("document '{0}:{1}' deleted", teamProjectCollectionId, sourceControlPath);
                }
                else
                {
                    _logger.Trace("Document:'{0}:{1}' was not found in the index", teamProjectCollectionId, sourceControlPath);
                }
            }
            catch (Exception ex)
            {
                _logger.ErrorException("Exception thrown", ex);
                throw;
            }
        }

        internal static string GetDocumentKey(string teamProjectCollectionId, string sourceControlPath)
        {
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId)) throw new ArgumentNullException("teamProjectCollectionId");
            if (string.IsNullOrWhiteSpace(sourceControlPath)) throw new ArgumentNullException("sourceControlPath");
            return string.Format("{0}:{1}", teamProjectCollectionId, sourceControlPath);
        }

        internal static string GetTeamProjectCollectionIdFromDocumentKey(string documentKey)
        {
            if (string.IsNullOrWhiteSpace(documentKey)) throw new ArgumentNullException("documentKey");
            var parts = documentKey.Split(':');
            if (parts == null || parts.Length != 2)
            {
                throw new ArgumentOutOfRangeException("No ':' in documentKey");
            }
            return parts[0];
        }

        internal static string GetPathFromDocumentKey(string documentKey)
        {
            if (string.IsNullOrWhiteSpace(documentKey)) throw new ArgumentNullException("documentKey");
            var parts = documentKey.Split(':');
            if (parts == null || parts.Length != 2)
            {
                throw new ArgumentOutOfRangeException("No ':' in documentKey");
            }
            return parts[1];
        }

        /// <summary>
        /// True if we can process this file
        /// </summary>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public bool IsSupportedFileExtension(string fileName)
        {
            var extension = Path.GetExtension(fileName);
            return _knownFileExtensions.Contains(extension);
        }

        /// <summary>
        /// Add this document with changeset and workitem information
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="sourceControlPath"></param>
        /// <returns></returns>
        public bool Add(IndexWriter writer, string teamProjectCollectionId, string sourceControlPath)
        {
            if (string.IsNullOrWhiteSpace(sourceControlPath)) throw new ArgumentNullException("sourceControlPath");
            _logger.Trace("teamProjectCollectionId:{0} sourceControlPath:{1}", teamProjectCollectionId, sourceControlPath);

            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
            }

            var extension = Path.GetExtension(sourceControlPath);
            if (string.IsNullOrWhiteSpace(extension))
            {
                _logger.Trace("file {0} discarded as it has no extension", sourceControlPath);
                return false;
            }

            if (!_knownFileExtensions.Contains(extension))
            {
                _logger.Trace("Not processing unknown fileextension {0}", sourceControlPath);
                return false;
            }

            var vcs = TfsSourceControl.Instance.GetVersionControlServer(teamProjectCollectionId);

            // Incoming changesets are a point in time, future changesets may have deleted the file
            if (!vcs.ServerItemExists(sourceControlPath, Microsoft.TeamFoundation.VersionControl.Client.ItemType.File))
            {
                _logger.Info("The item {0} may have been deleted", sourceControlPath);
                return false;
            }
            var item = vcs.GetItem(sourceControlPath);
            if (item == null)
            {
                return false;
            }

            var documentId = LuceneDocumentId(teamProjectCollectionId, sourceControlPath);
            if (documentId >= 0)
            {
                _logger.Trace("Document:{0}:{1} was found with id:{2}", teamProjectCollectionId, sourceControlPath, documentId);

                // Determine if we need to update this document, i.e. delete it first
                var document = _searcher.Doc(documentId);
                var changesets = document.GetValues(SourceControlDocument.FieldNames.ChangesetId);
                if (changesets != null && changesets.Length > 0)
                {
                    if (!changesets.Contains(item.ChangesetId.ToString()))
                    {
                        _logger.Trace("Item:{0} not found with changeset-id {1}, deleting prior to adding (an update)", sourceControlPath, item.ChangesetId);
                        Delete(writer, teamProjectCollectionId, sourceControlPath);
                    }
                    else
                    {
                        _logger.Trace("item '{0}' was not updated, not adding it to the index", sourceControlPath);
                        return false;
                    }
                }
            }
            writer.AddDocument(SourceControlDocument.Document(item, teamProjectCollectionId, sourceControlPath));
            _logger.Trace("item '{0}' was added", sourceControlPath);
            return true;
        }

        /// <summary>
        /// Get the Lucene document id
        /// </summary>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="sourceControlPath"></param>
        /// <returns></returns>
        public int LuceneDocumentId(string teamProjectCollectionId, string sourceControlPath)
        {
            if (string.IsNullOrWhiteSpace(sourceControlPath)) throw new ArgumentNullException("sourceControlPath");
            _logger.Trace("sourceControlPath:{0}:{1}", teamProjectCollectionId, sourceControlPath);

            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
            }

            int luceneDocumentId = -1;
            var term = new Term(SourceControlDocument.FieldNames.DocumentKey, GetDocumentKey(teamProjectCollectionId, sourceControlPath));
            var termDocs = _reader.TermDocs(term);
            while (termDocs.Next())
            {
                luceneDocumentId = termDocs.Doc();
                _logger.Trace("number found for '{0}' is {1}", sourceControlPath, luceneDocumentId);
                break;
            }
            return luceneDocumentId;
        }

        private bool Exists(string teamProjectCollectionId, string sourceControlPath)
        {
            return (LuceneDocumentId(teamProjectCollectionId, sourceControlPath) >= 0);
        }

        /// <summary>
        /// Get the documents that satisfy the given query
        /// </summary>
        /// <param name="field"></param>
        /// <param name="userQuery"></param>
        /// <returns></returns>
        public ScoreDoc[] Query(string field, string userQuery)
        {
            if (string.IsNullOrWhiteSpace(field)) throw new ArgumentNullException("field");
            if (string.IsNullOrWhiteSpace(userQuery)) throw new ArgumentNullException("userQuery");

            if (_cancelToken.IsCancellationRequested)
            {
                _logger.Trace("Cancellation requested");
                return null;
            }

            var parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_29, field, _analyzer);
            try
            {
                var query = parser.Parse(userQuery);
                var collector = TopScoreDocCollector.create(100, false);
                _searcher.Search(query, collector);
                var scoreDocs = collector.TopDocs().ScoreDocs;
                return scoreDocs;
            }
            catch (ParseException ex)
            {
                _logger.Trace("ParseException: {0}", ex);
            }
            return null;
        }

        /// <summary>
        /// Executes the query and returns Max 100 documents found
        /// </summary>
        /// <param name="userQuery"></param>
        /// <returns></returns>
        public QueryResultDTO Query(string userQuery)
        {
            int id;
            ScoreDoc[] changesetIdDocuments = null;
            ScoreDoc[] workitemIdDocuments = null;
            ScoreDoc[] changesetCommentDocuments = null;
            ScoreDoc[] changesetCommitterDocuments = null;
            ScoreDoc[] workitemTitleDocuments = null;
            ScoreDoc[] contentDocuments = null;

            if (int.TryParse(userQuery, out id))
            {
                changesetIdDocuments = Query(SourceControlDocument.FieldNames.ChangesetId, userQuery);
                workitemIdDocuments = Query(SourceControlDocument.FieldNames.WorkitemId, userQuery);
            }
            changesetCommentDocuments = Query(SourceControlDocument.FieldNames.ChangesetComment, userQuery);
            changesetCommitterDocuments = Query(SourceControlDocument.FieldNames.ChangesetCommitter, userQuery);
            workitemTitleDocuments = Query(SourceControlDocument.FieldNames.WorkitemTitle, userQuery);
            contentDocuments = Query(SourceControlDocument.FieldNames.Contents, userQuery);

            if ((changesetIdDocuments == null || changesetIdDocuments.Length == 0) &&
                (changesetCommentDocuments == null || changesetCommentDocuments.Length == 0) &&
                (changesetCommitterDocuments == null || changesetCommitterDocuments.Length == 0) &&
                (workitemIdDocuments == null || workitemIdDocuments.Length == 0) &&
                (workitemTitleDocuments == null || workitemTitleDocuments.Length == 0) &&
                (contentDocuments == null || contentDocuments.Length == 0))
            {
                // No documents found
                return null;
            }

            List<ScoreDoc> mergedList = new List<ScoreDoc>();
            if (changesetIdDocuments != null && changesetIdDocuments.Length > 0)
            {
                mergedList.AddRange(changesetIdDocuments);
            }
            if (changesetCommitterDocuments != null && changesetCommitterDocuments.Length > 0)
            {
                mergedList.AddRange(changesetCommitterDocuments);
            }
            if (changesetCommentDocuments != null && changesetCommentDocuments.Length > 0)
            {
                mergedList.AddRange(changesetCommentDocuments);
            }
            if (workitemIdDocuments != null && workitemIdDocuments.Length > 0)
            {
                mergedList.AddRange(workitemIdDocuments);
            }
            if (workitemTitleDocuments != null && workitemTitleDocuments.Length > 0)
            {
                mergedList.AddRange(workitemTitleDocuments);
            }
            if (contentDocuments != null && contentDocuments.Length > 0)
            {
                mergedList.AddRange(contentDocuments);
            }
            if (mergedList != null && mergedList.Count() > 0)
            {
                var sortedDocumentList = (from p in mergedList orderby p.score descending select p).Distinct(_scoreDocComparer).Take(100);
                var queryResultDTO = QueryResult.GetQueryResultDTO(from p in sortedDocumentList select _searcher.Doc(p.doc));
                return queryResultDTO;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Commit and optimize for each changeset
        /// </summary>
        /// <param name="writer"></param>
        internal void Commit(IndexWriter writer)
        {
            _logger.Trace("enter");
            _logger.Info("Waiting for writer.Optimize...");
            writer.Optimize();
            _logger.Info("Done");
            writer.Close(true);
            var newReader = _reader.Reopen();
            if (newReader != _reader)
            {
                _logger.Info("_reader is not current, creating a new reader and searcher...");
                _searcher.Close();
                _reader.Close();
                _reader = newReader;
                _searcher = new IndexSearcher(_reader);
                _logger.Info("Done");
            }
            _logger.Trace("exit");
        }

        /// <summary>
        /// Gets the number of documents in the index
        /// </summary>
        public int NumberOfDocuments
        {
            get { return _reader.NumDocs(); }
        }

        internal QueryResultDTO GetDocument(int documentId)
        {
            if (documentId < _reader.NumDocs())
            {
                if (!_reader.IsDeleted(documentId))
                {
                    var document = _searcher.Doc(documentId);
                    if (document != null)
                    {
                        return QueryResult.GetQueryResultDTO(new Document[] { document });
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Get the contents of this file
        /// </summary>
        /// <param name="teamProjectCollectionId"></param>
        /// <param name="filePath"></param>
        /// <returns></returns>
        internal string GetFile(string teamProjectCollectionId, string filePath)
        {
            if (string.IsNullOrWhiteSpace(teamProjectCollectionId) || teamProjectCollectionId.Length == 0)
            {
                _logger.Warn("teamProjectCollectionId is invalid");
                return null;
            }

            if (string.IsNullOrWhiteSpace(filePath) || filePath.Length == 0)
            {
                _logger.Warn("filePath is invalid");
                return null;
            }

            var documentId = LuceneDocumentId(teamProjectCollectionId, filePath);
            if (documentId == -1)
            {
                _logger.Warn("the file specified with {0}:{1} was not found", teamProjectCollectionId, filePath);
                return null;
            }
            return TfsSourceControl.Instance.GetFile(teamProjectCollectionId, filePath);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_reader != null)
                {
                    _reader.Dispose();
                    _reader = null;
                }
                if (_searcher != null)
                {
                    _searcher.Dispose();
                    _searcher = null;
                }
            }
        }

        /// <summary>
        /// Go away
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}